package ctu.kfad.run.iterpreter;

import ctu.kfad.run.EmptyThrowExceptionVisitor;
import ctu.kfad.run.InstanceController;
import ctu.kfad.run.Util;
import ctu.kfad.run.memory.IStackObjectProvider;
import ctu.kfad.run.stack.AStackObject;
import ctu.kfad.run.stack.IntegerStackObject;
import ctu.kfad.run.stack.ReferenceStackObject;
import org.apache.bcel.classfile.*;
import org.apache.bcel.generic.*;

import java.util.*;

public class Interpreter extends EmptyThrowExceptionVisitor implements IStackObjectProvider {

    private final JavaClass javaClass;
    private final ConstantPoolGen constantPoolGen;
    private final Map<Integer, AStackObject> localVariables;
    private final Stack<AStackObject> stack = new Stack<AStackObject>();
    private final InstanceController instanceController;
    private final InstructionList instructionList;
    private final Interpreter parentInterpreter;

    private boolean instructionJump = false;
    private boolean returnNull = false;
    private boolean returnStackHead = false;

    private InstructionHandle instructionHandle;

    public Interpreter(Interpreter parentInterpreter, InstanceController instanceController, JavaClass javaClass, Method method, Stack<AStackObject> initLocalVariables) {
        this(parentInterpreter, instanceController, javaClass, method);
        final int argumentsSize = initLocalVariables.size();
        for (int i = 0; i < argumentsSize; i++) {
            localVariables.put(i, initLocalVariables.pop());
        }
    }

    public Interpreter(Interpreter parentInterpreter, InstanceController instanceController, JavaClass javaClass, Method method) {
        localVariables = new HashMap<>();
        constantPoolGen = new ConstantPoolGen(javaClass.getConstantPool());
        instructionList = new InstructionList(method.getCode().getCode());   //save instruction list for easier debugging
        instructionHandle = instructionList.getStart();
        this.javaClass = javaClass;
        this.instanceController = instanceController;
        this.parentInterpreter = parentInterpreter;
    }

    public AStackObject interpret() {
        if (instructionHandle == null || returnStackHead || returnNull) {
            throw new RuntimeException("interpret was called");
        }

        while (true) {
            instructionHandle.accept(this);

            if (returnNull) {
                return null;
            }
            if (returnStackHead) {
                return stack.pop();
            }

            if (instructionJump) {
                instructionJump = false;
                // handle is set from jump instruction
            } else {
                instructionHandle = instructionHandle.getNext();
            }

            if (instructionHandle == null) {
                throw new RuntimeException("unexpected program");
            }
        }
    }

    private void jump(InstructionHandle target) {
        instructionHandle = target;
        instructionJump = true;
    }

    @Override
    public void visitICONST(ICONST iconst) {
        stack.push(new IntegerStackObject(iconst.getValue().intValue()));
    }

    @Override
    public void visitIADD(IADD obj) {
        stack.push(new IntegerStackObject(((IntegerStackObject) stack.pop()).getIntValue() + ((IntegerStackObject) stack.pop()).getIntValue()));
    }

    @Override
    public void visitISUB(ISUB obj) {
        int isub2 = ((IntegerStackObject) stack.pop()).getIntValue();
        int isub1 = ((IntegerStackObject) stack.pop()).getIntValue();
        stack.push(new IntegerStackObject(isub1 - isub2));
    }

    @Override
    public void visitIMUL(IMUL obj) {
        final int o1 = ((IntegerStackObject) stack.pop()).getIntValue();
        final int o2 = ((IntegerStackObject) stack.pop()).getIntValue();
        stack.push(new IntegerStackObject(o1 * o2));
    }

    @Override
    public void visitIDIV(IDIV obj) {
        int idiv2 = ((IntegerStackObject) stack.pop()).getIntValue();
        int idiv1 = ((IntegerStackObject) stack.pop()).getIntValue();
        stack.push(new IntegerStackObject(idiv1 / idiv2));
    }

    @Override
    public void visitIF_ICMPNE(IF_ICMPNE if_icmpne) {
        int if_icmpne2 = ((IntegerStackObject) stack.pop()).getIntValue();
        int if_icmpne1 = ((IntegerStackObject) stack.pop()).getIntValue();
        if (if_icmpne1 != if_icmpne2) {
            jump(if_icmpne.getTarget());
        }
    }

    @Override
    public void visitIF_ICMPEQ(IF_ICMPEQ if_icmpeq) {
        int if_icmpeq2 = ((IntegerStackObject) stack.pop()).getIntValue();
        int if_icmpeq1 = ((IntegerStackObject) stack.pop()).getIntValue();
        if (if_icmpeq1 == if_icmpeq2) {
            jump(if_icmpeq.getTarget());
        }
    }

    @Override
    public void visitIF_ICMPGE(IF_ICMPGE if_icmpge) {
        int if_icmpge2 = ((IntegerStackObject) stack.pop()).getIntValue();
        int if_icmpge1 = ((IntegerStackObject) stack.pop()).getIntValue();
        if (if_icmpge1 >= if_icmpge2) {
            jump(if_icmpge.getTarget());
        }
    }

    @Override
    public void visitGOTO(GOTO aGoto) {
        jump(aGoto.getTarget());
    }

    @Override
    public void visitISTORE(ISTORE istore) {
        localVariables.put(istore.getIndex(), stack.pop());
    }

    @Override
    public void visitBIPUSH(BIPUSH bipush) {
        stack.push(new IntegerStackObject(bipush.getValue().intValue()));
    }

    @Override
    public void visitIINC(IINC iinc) {
        final AStackObject aStackObject = localVariables.get(iinc.getIndex());
        int newValue = ((IntegerStackObject) aStackObject).getIntValue() + iinc.getIncrement();
        localVariables.put(iinc.getIndex(), new IntegerStackObject(newValue));
    }

    @Override
    public void visitASTORE(ASTORE astore) {
        localVariables.put(astore.getIndex(), stack.pop());
    }

    @Override
    public void visitALOAD(ALOAD aload) {
        stack.push(localVariables.get(aload.getIndex()));
    }

    @Override
    public void visitILOAD(ILOAD iload) {
        stack.push(localVariables.get(iload.getIndex()));
    }

    @Override        //push a constant #index from a constant pool (String, int or float) onto the stack
    public void visitLDC(LDC ldc) {
        final ConstantPool constantPool = javaClass.getConstantPool();
        final Constant constant = constantPool.getConstant(ldc.getIndex());
        if (constant instanceof ConstantString) {
            //----------------------------------------------------------------------------
            //replace with        (String)ldc.getValue(constantPoolGen) ?
            final int stringIndex = ((ConstantString) constant).getStringIndex();
            final Constant constantValue = constantPool.getConstant(stringIndex);
            final ConstantUtf8 constantValueInUTF = (ConstantUtf8) constantValue;
            final String string = constantValueInUTF.getBytes();
            //----------------------------------------------------------------------------
            final ReferenceStackObject stringReferenceStackObject = instanceController.saveString(string, this);
            stack.add(stringReferenceStackObject);
        } else if (constant instanceof ConstantInteger) {
            stack.add(new IntegerStackObject((Integer) ldc.getValue(constantPoolGen)));
        } else {
            throw new UnsupportedOperationException("unknown constant type");
        }
    }

    @Override
    public void visitRETURN(RETURN obj) {
        returnNull = true;
        returnStackHead = false; //fix wrong set in visitReturnInstruction
    }

    @Override
    public void visitReturnInstruction(ReturnInstruction obj) {
        returnStackHead = true;
    }

    @Override
    public void visitINVOKESTATIC(INVOKESTATIC invokestatic) {
        final InvokeInstructionCrate parser = parseInvokeInstruction(invokestatic);
        invokeMethod(parser);
    }

    @Override
    public void visitINVOKESPECIAL(INVOKESPECIAL invokespecial) {
        if (InstanceController.JAVA_OBJECT.equals(invokespecial.getLoadClassType(constantPoolGen).getClassName())) {
            stack.pop();  //we need to clear stack
            return;
        }
        final InvokeInstructionCrate parser = parseInvokeInstruction(invokespecial);
        parser.getArguments().push(stack.pop());  //reference to instance
        invokeMethod(parser);
    }

    private void invokeMethod(InvokeInstructionCrate parser) {
        final AStackObject interpretResult;
        if (parser.getMethod().isNative()) {
            final NativeInterpreter nativeInterpreter = new NativeInterpreter(instanceController, this);
            interpretResult = nativeInterpreter.interpret(parser.getCalledClass(), parser.getMethod(), parser.getArguments());
        } else {
            final Interpreter interpreter = new Interpreter(this, instanceController, parser.getCalledClass(), parser.getMethod(), parser.getArguments());
            interpretResult = interpreter.interpret();
        }
        if (interpretResult != null) {
            stack.push(interpretResult);
        }
    }

    @Override
    public void visitINVOKEVIRTUAL(INVOKEVIRTUAL invokevirtual) {
        final String className = invokevirtual.getLoadClassType(constantPoolGen).getClassName();
        if (InstanceController.JAVA_OBJECT.equals(className) || InstanceController.JAVA_STRING.equals(className)) {
            processCustomObjectMethod(invokevirtual);
            return;
        }
        final InvokeInstructionCrate parser = parseInvokeInstruction(invokevirtual);
        parser.getArguments().push(stack.pop());  //reference to instance
        invokeMethod(parser);
    }

    private void processCustomObjectMethod(INVOKEVIRTUAL invokevirtual) {
        final String methodName = invokevirtual.getMethodName(constantPoolGen);
        switch (methodName) {
            case "toString":
                final ReferenceStackObject stringReferenceStackObject = instanceController.objectToString((ReferenceStackObject) stack.pop(), this);
                stack.push(stringReferenceStackObject);
                break;
            case "equals":
                final ReferenceStackObject pop1 = (ReferenceStackObject) stack.pop();
                final ReferenceStackObject pop2 = (ReferenceStackObject) stack.pop();
                stack.push(instanceController.objectEquals(pop1, pop2));
                break;
            case "hashCode":
                final IntegerStackObject integerStackObject = instanceController.objectHashCode((ReferenceStackObject) stack.pop());
                stack.push(integerStackObject);
                break;
            default:
                throw new RuntimeException(methodName + " is not implemented");
        }
    }

    @Override
    public void visitNEW(NEW newInst) {
        final ObjectType classType = newInst.getLoadClassType(constantPoolGen);
        ReferenceStackObject reference = instanceController.createInstance(classType, this);
        stack.push(reference);

    }

    @Override
    public void visitDUP(DUP dup) {
        final AStackObject pop = stack.pop();
        stack.push(pop);
        stack.push(pop);
    }

    @Override
    public void visitNEWARRAY(NEWARRAY newarray) {
        final Type type = newarray.getType();
        final int size = ((IntegerStackObject) stack.pop()).getIntValue();
        ReferenceStackObject reference = instanceController.createArrayInstance((ArrayType) type, size, this);
        stack.push(reference);
    }

    @Override
    public void visitANEWARRAY(ANEWARRAY anewarray) {
        final int size = ((IntegerStackObject) stack.pop()).getIntValue();
        ReferenceStackObject reference = instanceController.createReferenceArrayInstance(size, this);
        stack.push(reference);
    }

    @Override
    public void visitSIPUSH(SIPUSH sipush) {
        stack.push(new IntegerStackObject(sipush.getValue().intValue()));
    }

    /**
     * store an int into an array
     *
     * @param iastore
     */
    @Override
    public void visitIASTORE(IASTORE iastore) {
        final IntegerStackObject value = (IntegerStackObject) stack.pop();
        final IntegerStackObject index = (IntegerStackObject) stack.pop();
        final ReferenceStackObject referenceStackObject = (ReferenceStackObject) stack.pop();
        if (index.getIntValue() < 0 || index.getIntValue() > instanceController.arrayLength(referenceStackObject)) {
            throw new ArrayIndexOutOfBoundsException();
        }
        instanceController.arrayStore(referenceStackObject, index.getIntValue(), value.getIntValue());
    }

    @Override
    public void visitAASTORE(AASTORE obj) {
        final ReferenceStackObject valueReferenceStackObject = (ReferenceStackObject) stack.pop();
        final IntegerStackObject index = (IntegerStackObject) stack.pop();
        final ReferenceStackObject arrayReferenceStackObject = (ReferenceStackObject) stack.pop();

        if (index.getIntValue() < 0 || index.getIntValue() > instanceController.arrayLength(arrayReferenceStackObject)) {
            throw new ArrayIndexOutOfBoundsException();
        }

        //TODO mozna rozsirit API o reference
        instanceController.arrayStore(arrayReferenceStackObject, index.getIntValue(), valueReferenceStackObject.getReference());
    }

    @Override
    public void visitIALOAD(IALOAD iaload) {
        final IntegerStackObject index = (IntegerStackObject) stack.pop();
        final ReferenceStackObject referenceStackObject = (ReferenceStackObject) stack.pop();
        if (index.getIntValue() < 0 || index.getIntValue() > instanceController.arrayLength(referenceStackObject)) {
            throw new ArrayIndexOutOfBoundsException();
        }
        int value = instanceController.arrayLoadInteger(referenceStackObject, index.getIntValue());
        stack.push(new IntegerStackObject(value));
    }

    @Override
    public void visitAALOAD(AALOAD obj) {
        final IntegerStackObject index = (IntegerStackObject) stack.pop();
        final ReferenceStackObject arrayReferenceStackObject = (ReferenceStackObject) stack.pop();

        if (index.getIntValue() < 0 || index.getIntValue() > instanceController.arrayLength(arrayReferenceStackObject)) {
            throw new ArrayIndexOutOfBoundsException();
        }

        //TODO mozna rozsirit API o reference
        final int value = instanceController.arrayLoadInteger(arrayReferenceStackObject, index.getIntValue());
        stack.push(new ReferenceStackObject(value));
    }

    @Override
    public void visitARRAYLENGTH(ARRAYLENGTH arraylength) {
        final ReferenceStackObject referenceStackObject = (ReferenceStackObject) stack.pop();
        stack.push(new IntegerStackObject(instanceController.arrayLength(referenceStackObject)));
    }

    @Override
    public void visitPUTFIELD(PUTFIELD obj) {
        final AStackObject value = stack.pop();
        final ReferenceStackObject reference = (ReferenceStackObject) stack.pop();
        final ObjectType loadClassType = obj.getLoadClassType(constantPoolGen);
        //TODO check: I don't know if I should use obj.getLoadClassType or obj.getReferenceType . It will be different when we will use inheritance
        // class from interpreter   in private case, but in public?
        instanceController.putField(reference, loadClassType, obj.getFieldName(constantPoolGen), value);
    }

    @Override
    public void visitGETFIELD(GETFIELD obj) {
        final ReferenceStackObject reference = (ReferenceStackObject) stack.pop();
        final ObjectType loadClassType = obj.getLoadClassType(constantPoolGen);
        stack.push(instanceController.getField(reference, loadClassType, obj.getFieldName(constantPoolGen)));
    }

    @Override
    public void visitDUP2(DUP2 obj) {
        final AStackObject o1 = stack.pop();
        final AStackObject o2 = stack.pop();
        stack.push(o2);
        stack.push(o1);
        stack.push(o2);
        stack.push(o1);
    }


    @Override
    public void visitIAND(IAND obj) {
        final IntegerStackObject pop1 = (IntegerStackObject) stack.pop();
        final IntegerStackObject pop2 = (IntegerStackObject) stack.pop();
        stack.push(new IntegerStackObject(pop1.getIntValue() & pop2.getIntValue()));
    }

    @Override
    public void visitIFEQ(IFEQ obj) {  //if value is 0, branch to instruction at branchoffset
        final IntegerStackObject pop = (IntegerStackObject) stack.pop();
        if (pop.getIntValue() == 0) {
            jump(obj.getTarget());
        }
    }

    @Override
    public void visitIOR(IOR obj) {
        final IntegerStackObject pop1 = (IntegerStackObject) stack.pop();
        final IntegerStackObject pop2 = (IntegerStackObject) stack.pop();
        stack.push(new IntegerStackObject(pop1.getIntValue() | pop2.getIntValue()));
    }

    @Override
    public void visitIFNE(IFNE obj) {
        final IntegerStackObject pop = (IntegerStackObject) stack.pop();
        if (pop.getIntValue() != 0) {
            jump(obj.getTarget());
        }
    }

    @Override
    public void visitIF_ACMPNE(IF_ACMPNE obj) {
        final ReferenceStackObject pop1 = (ReferenceStackObject) stack.pop();
        final ReferenceStackObject pop2 = (ReferenceStackObject) stack.pop();
        if (pop1.getReference() != pop2.getReference()) {
            jump(obj.getTarget());
        }
    }

    @Override
    public void visitIF_ACMPEQ(IF_ACMPEQ obj) {
        final ReferenceStackObject pop1 = (ReferenceStackObject) stack.pop();
        final ReferenceStackObject pop2 = (ReferenceStackObject) stack.pop();
        if (pop1.getReference() == pop2.getReference()) {
            jump(obj.getTarget());
        }
    }

    @Override
    public void visitBASTORE(BASTORE obj) {
        final IntegerStackObject value = (IntegerStackObject) stack.pop();
        final IntegerStackObject index = (IntegerStackObject) stack.pop();
        final ReferenceStackObject ref = (ReferenceStackObject) stack.pop();
        if (index.getIntValue() < 0 || index.getIntValue() > instanceController.arrayLength(ref)) {
            throw new ArrayIndexOutOfBoundsException();
        }
        instanceController.arrayStore(ref, index.getIntValue(), (byte) value.getIntValue());
    }

    @Override
    public void visitBALOAD(BALOAD obj) {
        final IntegerStackObject index = (IntegerStackObject) stack.pop();
        final ReferenceStackObject ref = (ReferenceStackObject) stack.pop();
        if (index.getIntValue() < 0 || index.getIntValue() > instanceController.arrayLength(ref)) {
            throw new ArrayIndexOutOfBoundsException();
        }
        byte b = instanceController.arrayLoadByte(ref, index.getIntValue());
        stack.push(new IntegerStackObject((int) b));
    }

    public Interpreter getParentInterpreter() {
        return parentInterpreter;
    }

    public InvokeInstructionCrate parseInvokeInstruction(InvokeInstruction invoke) {
        final String methodName = invoke.getMethodName(constantPoolGen);
        final ObjectType classType = invoke.getLoadClassType(constantPoolGen);
        final String signature = invoke.getSignature(constantPoolGen);

        final JavaClass invokeClass = instanceController.loadClass(classType.getClassName());

        MethodClassCrate methodClass = Util.findMethodClassByNameAndSignature(methodName, null, signature, invokeClass);

        final int argumentCount = methodClass.getMethod().getArgumentTypes().length;
        Stack<AStackObject> arguments = new Stack<>();
        for (int i = 0; i < argumentCount; i++) {
            arguments.push(stack.pop());
        }

        if (invoke instanceof INVOKEVIRTUAL) {     //SPECIAL are privates and constructors
            JavaClass instanceClass = instanceController.loadClass((ReferenceStackObject) stack.peek());
            methodClass = Util.findMethodClassByNameAndSignature(methodName, instanceClass, signature, invokeClass);
        }

        return new InvokeInstructionCrate(methodClass.getMethod(), methodClass.getCalledClass(), arguments);
    }

    @Override
    public List<AStackObject> getStackObjects() {
        final ArrayList<AStackObject> result = new ArrayList<>();
        Collections.copy(stack, result);
        result.addAll(localVariables.values());

        if (parentInterpreter != null) {
            result.addAll(parentInterpreter.getStackObjects());
        }
        return result;
    }

    @Override
    public void visitIF_ICMPLE(IF_ICMPLE if_icmple) {
        final IntegerStackObject o1 = (IntegerStackObject) stack.pop();
        final IntegerStackObject o2 = (IntegerStackObject) stack.pop();
        if (o1.getIntValue() >= o2.getIntValue()) {
            jump(if_icmple.getTarget());
        }
    }

    @Override
    public void visitIF_ICMPGT(IF_ICMPGT obj) {
        final IntegerStackObject o1 = (IntegerStackObject) stack.pop();
        final IntegerStackObject o2 = (IntegerStackObject) stack.pop();
        if (o1.getIntValue() < o2.getIntValue()) {
            jump(obj.getTarget());
        }
    }

    @Override
    public void visitIFGE(IFGE obj) {
        final IntegerStackObject o1 = (IntegerStackObject) stack.pop();
        if (o1.getIntValue() > 0) {
            jump(obj.getTarget());
        }
    }

    @Override
    public void visitACONST_NULL(ACONST_NULL aconst_null) {
        stack.push(new ReferenceStackObject(0));
    }

    @Override
    public void visitIFNONNULL(IFNONNULL ifnonnull) {
        final ReferenceStackObject pop = (ReferenceStackObject) stack.pop();
        if (pop.getReference() != 0) {
            jump(ifnonnull.getTarget());
        }
    }

    @Override
    public void visitIFNULL(IFNULL ifnull) {
        final ReferenceStackObject pop = (ReferenceStackObject) stack.pop();
        if (pop.getReference() == 0) {
            jump(ifnull.getTarget());
        }
    }
}
